﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GameLibrary.Map
{
    using Unit;
    using Settlement;
    using Events;
    [Serializable]
    public class PlayerMap
    {
        #region variables

        /// <summary>
        /// represent a map
        /// </summary>
        private RegionMap knownMap;

        /// <summary>
        /// Player's ID
        /// </summary>
        public uint playerID { get; private set; }

        /// <summary>
        /// Connection to nearby city
        /// 1st position -- from
        /// 2nd position -- to
        /// </summary>
        public RoadNetwork roadMap { get; private set; }

        #endregion

        #region constructor
        public PlayerMap(uint playerID, Position firstRegion)
        {
            this.knownMap = new RegionMap();
            knownMap.RefreshRegion(firstRegion);
            this.playerID = playerID;
            this.roadMap = new RoadNetwork();
        }
        #endregion

        #region event
        private void OwnershipChanged(object sender, OwnershipChangedEventArgs e)
        {
            // refresh road situation and city situation
            if(e.Obj is Settlement)
                if (e.OldOwner == this.playerID) // city get by other player
                {
                    this.knownMap.RefreshSettlement(e.Obj as Settlement);
                    this.roadMap.RemovePosition(e.Obj.Location);
                }
                else
                {
                    if (e.NewOwner == this.playerID)
                    {
                        this.AddSettlement(e.Obj as Settlement);
                    }
                }                    
        }
        private void LocationChanged(object sender, LocationChangedEventArgs e)
        {
            if (e.Obj.OwnerID == this.playerID) // only change object info when it belong to this player
            {                
                this.AddUnitWithoutConnectEvent(e.Obj as Unit);
            }
        }

        private void LocationChanging(object sender, LocationChangingEventArgs e)
        {
            if (e.Obj.OwnerID == this.playerID)
            {
                this.RemoveUnitWithoutDisconnectEvent(e.Obj as Unit);
            }
        }

        private void BuildingLevelDecreased(object sender, BuildingLevelDecreasedEventArgs e)
        {
            this.RefreshSettlement(e.Settlement as Settlement);
        }

        #endregion

        #region add
        internal void AddUnit(Unit unit)
        {
            this.knownMap.AddUnit(unit);

            this.ConnectEvent(unit);
        }
        private void AddUnitWithoutConnectEvent(Unit unit)
        {
            this.knownMap.AddUnit(unit);
        }
        internal void AddSettlement(Settlement settlement)
        {        
            this.knownMap.AddSettlement(settlement);
            settlement.OwnerChanged += this.OwnershipChanged;
            settlement.BuildingLevelDecreased += this.BuildingLevelDecreased;
            this.RefreshRoadNetwork(settlement);
            this.ConnectEvent(settlement);
        }
        #endregion

        #region remove

        internal void RemoveUnit(Unit unit)
        {
            this.knownMap.RemoveUnit(unit.ID);
           this.DisconnectEvent(unit);
        }
        private void RemoveUnitWithoutDisconnectEvent(Unit unit)
        {
            this.knownMap.RemoveUnit(unit.ID);
        }
        internal void RemoveSettlement(Settlement settlement)
        {
            this.knownMap.RemoveSettlement(settlement.ID);           
            settlement.OwnerChanged -= this.OwnershipChanged;
            settlement.BuildingLevelDecreased -= this.BuildingLevelDecreased;
            this.roadMap.RemovePosition(MapHelper.FindRegion(settlement.Location));
         //   this.DisconnectEvent(settlement);
        }

        private void ConnectEvent(Interfaces.IMapObjectWithEventHandler obj)
        {
            obj.LocationChanged += this.LocationChanged;
            obj.OwnerChanged += this.OwnershipChanged;
            obj.LocationChanging += this.LocationChanging;
        }

        private void DisconnectEvent(Interfaces.IMapObjectWithEventHandler obj)
        {
            obj.LocationChanging -= this.LocationChanging;
            obj.LocationChanged -= this.LocationChanged;
            obj.OwnerChanged -= this.OwnershipChanged;
        }
        #endregion

        #region get

        internal SettlementInfo GetSettlementInfoByPosition(Position pos)
        {
            return this.knownMap.GetSettlementInfoByPosition(pos);
        }

        /// <summary>
        /// return 
        /// </summary>
        /// <param name="regionPosition"></param>
        /// <returns></returns>
        public IEnumerable<RegionInfo> GetCurrentMapRegionInfo(
            IEnumerable<RegionInfo> regions)
        {
            List<RegionInfo> regs = new List<RegionInfo>();
            foreach (RegionInfo region in regions)
                regs.Add(GameMap.UniqueInstance.GetRegionInfo(region.Start));
            return regs;
        }

        /// <summary>
        /// Return a list of position which can be reached by units in unitlist 
        /// without fight.
        /// Require unit in unitlist in this region.
        /// </summary>
        /// <param name="region"></param>
        /// <param name="unitlist"></param>
        /// <returns></returns>
        public Dictionary<Position, Tile.TileType> GetFreePosition(
            RegionInfo region,
            List<Unit> unitlist)
        {
            var freePositions = region.GetFreePosition();

            Dictionary<Position, Tile.TileType> list = new Dictionary<Position, Tile.TileType>();
            foreach (var p in freePositions)
            {
                var reg = this.FindRegion(p);
                if (reg.GetUnitsByPosition(p).Count == 0
                    && reg.GetSettlementByPosition(p) == null)
                    list.Add(p, this.GetTileType(p));
            }
            return list;
        }
        /// <summary>
        /// Get tiletype of a position
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        public Tile.TileType GetTileType(Position pos)
        {
            var tile = GameMap.UniqueInstance.GetTileType(pos);
            return tile;
        }
        /// <summary>
        /// Get region contains position given
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        public RegionInfo GetRegionInfo(Position pos)
        {
            Position regionPosition = MapHelper.FindRegion(pos);
            try
            {              
                return this.knownMap.GetRegionInfo(regionPosition);
            }
            catch (KeyNotFoundException)
            {
                this.knownMap.RefreshRegion(pos);
                return this.knownMap.GetRegionInfo(regionPosition);
            }
        }
        public AdjancencyList<RegionInfo, RegionInfo> GetBorderAdjancencyList()
        {
            throw new Exception("Not Tested");
          //  return this.knownMap.GetBorderRegions(this.playerID);
        }
        public AdjancencyList<RegionInfo,RegionInfo> GetCurrentMap()
        {
            return this.knownMap.GetKnownMap();
        }
        /// <summary>
        /// Get shortest path between from and to.
        /// 
        /// Method: 
        ///     First move to the nearest city in this region,
        ///     then move to the city in the region contains to,
        ///     last move from that city to to.
        ///     
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns></returns>
        public Path GetPath(Position from, Position to)
        {    
            Position fromPos = new Position(from.X % GMHelper.RegionWidth,
                                                from.Y % GMHelper.RegionHeight);
           
            Position toPos = new Position(to.X % GMHelper.RegionWidth,
                                                to.Y % GMHelper.RegionHeight);
           
            RegionInfo regFrom = this.FindRegion(from);
            if (regFrom == null) // unknown region
                return null;
                
            RegionInfo regTo = this.FindRegion(to);
            if (regTo == null) // unknown region
                return null;

            if (regTo == regFrom) // within one region
            {
                var path = new
                    AI.ShortestPath.AStar(
                     regFrom.GetMoveCostMap(this.playerID), fromPos, toPos
                         ).GetResult();
                return new Path(path);
            }
            else
            {
                throw new NotImplementedException();
            }
        }
        public List<Path> GetAllPathes()
        {
            return this.roadMap.Values.ToList();
        }

        private RegionInfo FindRegion(Position pos)
        {
            return this.knownMap[MapHelper.FindRegion(pos)];
        }

        /// <summary>
        /// Return all the city which belong to a player in a region.
        /// Return null if no city belong to this player exist in that region. 
        /// </summary>
        /// <param name="regionPosition"></param>
        /// <returns></returns>
        public IEnumerable<Settlement> GetCityInARegion(Position regionPosition)
        {
            var cities = GameMap.UniqueInstance.GetRegionSettlements(regionPosition);
            if(cities.Count() == 0)
                return null;
            var city = from c in cities
                        where c.OwnerID == this.playerID
                        && c.Type == SettlementFlags.City
                        select c;
            if (city.Count() == 0)
                return null;
            else
                return city;
        }

        public HashSet<RegionInfo> GetAllRegionBelongToAPlayer(uint playerID)
        {
            var set = from region in this.knownMap.GetAllRegions()
                      where region.ContainsSettlementOwnerID(playerID) == true
                      select region;
            return new HashSet<RegionInfo>(set);
        }

        public double GetMoveCostOfAPosition(Position pos)
        {
            uint roadLevel = 1;
            var settlement = GameMap.UniqueInstance.GetSettlementByPosition(pos);
            if(settlement != null)
                roadLevel += settlement.GetBuildingLevel(SettlementHelper.Road);
            double cost = GameMap.UniqueInstance.GetTileType(pos).BaseMoveCost
                / roadLevel;
            return cost;
        }

        public Unit GetUnitByID(uint id)
        {
            return GameMap.UniqueInstance.GetUnitByID(id);
        }
        public Settlement GetSettlementByID(uint id)
        {
            return GameMap.UniqueInstance.GetSettlementByID(id);
        }

        public IEnumerable<UnitInfo> GetUnitInfosByPosition(Position pos)
        {
            var region = this.GetRegionInfo(pos);
            List<UnitInfo> list = region.GetUnitsByPosition(pos);
            return list;
        }
        #endregion

        #region refresh

        public void RefreshRegion(Position regionPosition)
        {
            RegionInfo region = this.GetRegionInfo(regionPosition);
            region.Refresh();
        }

        public void RefreshSettlement(Settlement settlement)
        {
            RegionInfo reg = this.GetRegionInfo(settlement.Location);
            reg.RefreshCity(settlement);
        }
        public void RefreshUnit(Unit unit)
        {
            RegionInfo reg = this.GetRegionInfo(unit.Location);
            reg.RefreshUnit(unit);
        }
        /// <summary>
        /// Add new road connections of this new city into road network
        /// </summary>
        /// <param name="city"></param>
        private void RefreshRoadNetwork(Settlement city)
        {
            Position cityregion = MapHelper.FindRegion(city.Location);
            if (this.knownMap.ContainsRegion(cityregion) == false) // we don't have this region in road network
            {
                List<Position> cities = new List<Position>();
                var regionList = this.knownMap.GetAdjancencyRegion(cityregion); // get all regions nearby
                foreach (var r in regionList)
                {
                    var citylist = r.GetSettlements(); // get all cities in each region
                    if (citylist != null)
                    {
                        citylist = from c in citylist
                                   where c.OwnerID == this.playerID
                                   select c;
                        cities.Add(citylist.First().Location); // only consider one city in any region
                    }
                }
                throw new NotImplementedException();
                // find path between new added city and its surrounding cities
            }
            // no else, because if we already visited this region the path of this region
            // already been found
        }
        #endregion

        #region costmap
        internal double[,] GetCostMap(RegionInfo region, IEnumerable<RegionInfo> surroundingRegions)
        {
            throw new NotImplementedException();
        }

        #endregion

    }
}
